package org.unidata.mdm.rest.data.service;

import java.text.ParseException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.ws.rs.Consumes;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.HttpMethod;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.PathSegment;
import javax.ws.rs.core.Response;

import com.google.common.collect.Multimap;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.unidata.mdm.core.service.MetaModelService;
import org.unidata.mdm.core.type.model.AttributeElement;
import org.unidata.mdm.data.context.RestoreRecordRequestContext;
import org.unidata.mdm.data.context.RestoreRecordRequestContext.RestoreRecordRequestContextBuilder;
import org.unidata.mdm.data.dto.EtalonRecordDTO;
import org.unidata.mdm.data.dto.RestoreRecordDTO;
import org.unidata.mdm.data.exception.DataConsistencyException;
import org.unidata.mdm.data.exception.DataProcessingException;
import org.unidata.mdm.data.service.DataRecordsService;
import org.unidata.mdm.meta.configuration.Descriptors;
import org.unidata.mdm.rest.data.converter.DataRecordEtalonConverter;
import org.unidata.mdm.rest.data.exception.DataRestExceptionIds;
import org.unidata.mdm.rest.data.ro.EtalonRecordRO;
import org.unidata.mdm.rest.data.type.rendering.DataRestInputRenderingAction;
import org.unidata.mdm.rest.data.util.RestUtils;
import org.unidata.mdm.rest.system.ro.ErrorInfo;
import org.unidata.mdm.rest.system.ro.ErrorResponse;
import org.unidata.mdm.rest.system.ro.RestResponse;
import org.unidata.mdm.rest.system.ro.UpdateResponse;
import org.unidata.mdm.rest.system.service.AbstractRestService;
import org.unidata.mdm.rest.system.util.RestConstants;
import org.unidata.mdm.system.dto.Param;
import org.unidata.mdm.system.service.RenderingService;
import org.unidata.mdm.system.service.TextService;
import org.unidata.mdm.system.type.rendering.MapInputSource;
import org.unidata.mdm.system.type.runtime.MeasurementContextName;
import org.unidata.mdm.system.type.runtime.MeasurementPoint;
import org.unidata.mdm.system.util.ConvertUtils;

/**
 * @author Mikhail Mikhailov on Jun 2, 2020
 */
@Path(RestoreRestService.SERVICE_PATH)
@Consumes({MediaType.APPLICATION_JSON})
@Produces({MediaType.APPLICATION_JSON})
public class RestoreRestService extends AbstractRestService {
    /**
     * Logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(RestoreRestService.class);
    /**
     * This service path.
     */
    public static final String SERVICE_PATH = "restore";

    public static final String PATH_VALIDATE = "pre-restore-validation";
    /**
     * Data records service.
     */
    @Autowired
    private DataRecordsService dataRecordsService;
    /**
     * Meta model service.
     */
    @Autowired
    private MetaModelService metaModelService;
    /**
     * The text service.
     */
    @Autowired
    private TextService textService;
    /**
     * The rendering service.
     */
    @Autowired
    private RenderingService renderingService;
    /**
     * Constructor.
     */
    public RestoreRestService() {
        super();
    }
    /**
     * Restore previously deleted period.
     *
     * @param id the record id.
     * @param timestamps the tomestamp as path segments
     * @return {@link RestResponse}
     * @throws Exception
     */
    @PUT
    @Path("/{" + RestConstants.DATA_PARAM_ID + "}/{" + RestConstants.DATA_PARAM_TIMESTAMPS + ":.*}")
    @Operation(
        description = "Restores period of a record by record ID and 'from' and 'to' boundaries.",
        method = HttpMethod.PUT,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response restorePeriod(
            @Parameter(description = "Record ID", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String id,
            @Parameter(description = "Draft id. Optional.", in = ParameterIn.QUERY)  @QueryParam("draftId") @DefaultValue("0") long draftId,
            @Parameter(description = "2 timestamps - ISO _LOCAL_ timestamps (without timezone)", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_TIMESTAMPS) List<PathSegment> timestamps) {

        final RestResponse<EtalonRecordRO> response = new RestResponse<>();

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_PERIOD_RESTORE);
        MeasurementPoint.start();
        try {

            Date validFrom = RestUtils.extractStart(timestamps);
            Date validTo = RestUtils.extractEnd(timestamps);

            RestoreRecordRequestContextBuilder ctxb = RestoreRecordRequestContext.builder()
                    .etalonKey(id)
                    .validFrom(validFrom)
                    .validTo(validTo)
                    .draftId(draftId)
                    .periodRestore(true);

            MapInputSource mis = new MapInputSource();
            mis.putString("id", id);
            mis.putDate("validFrom", validFrom);
            mis.putDate("validTo", validTo);

            renderingService.renderInput(DataRestInputRenderingAction.PERIOD_RESTORE_INPUT, ctxb, mis);

            EtalonRecordDTO result = dataRecordsService.restore(ctxb.build());

            // TODO Render output.

            response.setSuccess(true);
            response.setContent(DataRecordEtalonConverter.to(Objects.isNull(result) ? null : result.getEtalon(), Collections.emptyList()));

        } catch (Exception exc) {
            LOGGER.error("Can't restore record period.", exc);
            response.setSuccess(false);
            if (exc instanceof DataProcessingException) {
                ErrorInfo error = new ErrorInfo();
                error.setSeverity(ErrorInfo.Severity.LOW);
                error.setInternalMessage(exc.getMessage());
                error.setUserMessage(textService.getText(exc));
                response.setErrors(Arrays.asList(error));
                return error(response);
            }
        } finally {
            MeasurementPoint.stop();
        }

        return ok(response);
    }
    /**
     * Restore previously deleted record.
     *
     * @param record record id.
     * @return
     * @throws Exception
     */
    @PUT
    @Path("/{" + RestConstants.DATA_PARAM_ID + "}")
    @Operation(
        description = "Restore record at whole by ID. Only a full record is accepted!",
        method = HttpMethod.PUT,
        requestBody = @RequestBody(content = @Content(schema = @Schema(implementation = EtalonRecordRO.class)), description = "The record to restore."),
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = RestResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response restoreRecord(
            @Parameter(description = "Record ID", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            EtalonRecordRO record) {

        RestResponse<EtalonRecordRO> response = null;

        MeasurementPoint.init(MeasurementContextName.MEASURE_UI_RESTORE);
        MeasurementPoint.start();

        EtalonRecordRO retval = null;
        try {

            // try to restore given record.
            // It may fail in case if it's has references to inactive records.
            RestoreRecordRequestContextBuilder ctxb = RestoreRecordRequestContext.builder()
                    .record(DataRecordEtalonConverter.from(record))
                    .etalonKey(record.getEtalonId())
                    .modified(record.isModified());

            MapInputSource mis = new MapInputSource();
            mis.putString("id", record.getEtalonId());
            mis.put("record", record);

            renderingService.renderInput(DataRestInputRenderingAction.RECORD_RESTORE_INPUT, ctxb, mis);

            RestoreRecordDTO result = dataRecordsService.restore(ctxb.build());

            retval = DataRecordEtalonConverter.to(result.getEtalon(), null);
            response = new RestResponse<>(retval);

        } catch (DataConsistencyException dce) {

            List<ErrorInfo> errors = new ArrayList<>();
            ErrorInfo error = new ErrorInfo();
            error.setErrorCode(DataRestExceptionIds.EX_DATA_CANNOT_DELETE_REF_EXIST.code());
            error.setSeverity(ErrorInfo.Severity.LOW);
            error.setInternalMessage("Some lookup entities referenced by this record are missing!");
            error.setUserMessage("Повторите восстановление после проверки значений");
            errors.add(error);

            response = new RestResponse<>(retval);
            response.setSuccess(false);
            response.setErrors(errors);

        } catch (Exception exc) {
            retval = record;
            retval.setEntityType(metaModelService.instance(Descriptors.DATA).isRegister(record.getEntityName())
                    ? RestConstants.REGISTER_ENTITY_TYPE
                    : RestConstants.LOOKUP_ENTITY_TYPE);
            response = new RestResponse<>(retval);
            response.setSuccess(false);
        } finally {
            MeasurementPoint.stop();
        }

        return ok(response);
    }

    /**
     * Check that deleted record can be restored.
     *
     * @param etalonId etalon id.
     * @return
     * @throws ParseException
     */
    @GET
    @Path("/" + PATH_VALIDATE + "/" + "{" + RestConstants.DATA_PARAM_ID + "}{p:/?}{" + RestConstants.DATA_PARAM_DATE
            + ": " + RestConstants.DEFAULT_TIMESTAMP_PATTERN + "}")
    @Operation(
        description = "Checks, whether the record can be restored or not.",
        method = HttpMethod.GET,
        responses = {
            @ApiResponse(content = @Content(schema = @Schema(implementation = UpdateResponse.class)), responseCode = "200"),
            @ApiResponse(content = @Content(schema = @Schema(implementation = ErrorResponse.class)), responseCode = "500")
        }
    )
    public Response check(
            @Parameter(description = "Record ID", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_ID) String etalonId,
            @Parameter(description = "Timestamps", in = ParameterIn.PATH) @PathParam(RestConstants.DATA_PARAM_DATE) String dateAsString) {

        Date asOf = ConvertUtils.string2Date(dateAsString);
        Multimap<AttributeElement, Object> missedLookupEntities = null;/*validationService.getMissedLinkedLookupEntities(
                etalonId, asOf);*/// TODO @Modules
        if (missedLookupEntities == null || missedLookupEntities.size() == 0) {
            return Response.ok(new UpdateResponse(true, etalonId)).build();
        }

        UpdateResponse response = new UpdateResponse(false, etalonId);
        List<ErrorInfo> errors = new ArrayList<>();
        ErrorInfo error = new ErrorInfo();
        error.setErrorCode(DataRestExceptionIds.EX_DATA_CANNOT_DELETE_REF_EXIST.code());
        error.setSeverity(ErrorInfo.Severity.LOW);
        error.setInternalMessage("Some lookup entities referenced by this record are missing!");
        error.setUserMessage("Повторите восстановление после проверки значений");
        errors.add(error);
        response.setErrors(errors);
        List<Param> params = missedLookupEntities.entries()
                .stream()
                .filter(ent -> ent.getValue() != null)
                .map(ent -> new Param(ent.getKey().getPath(),
                        ent.getValue().toString()))
                .collect(Collectors.toList());
        error.setParams(params);
        return Response.ok(response).build();
    }
}
